年末の思い出をシェアしよう、Cognitoを使った写真共有サービスを作ろう – ClassmethodサーバーレスAdvent Calendar 2017 #serverless #adventcalendar
こんにちは、田中孝明です。
このエントリは Serverless Advent Calendar 2017 4日目の記事です。
いよいよ2017年も終わりが近づき、忘年会、クリスマス、年越しとイベントが目白押しではないでしょうか?
思い出のシェアとなるとInstagramやfacebook、Google Photoなど、様々な写真共有サービスが候補にあがるでしょう。
だた、中には限られた人たちにだけ公開したい、特別な思い出もあるかもしれません。
そんなあなたにとって、 Amazon Cognito と Amazon S3 は、希望を叶えてくれる力強い存在となるでしょう。
やりたいこと
- Amazon Cognito でユーザー認証
- 認証したユーザーでiOS端末から写真を送信
- 写真を Amazon S3 に保存
Amazon Cognitoを使った認証
Google認証
ユーザーの認証にGoogleアカウントを利用することにしました。
Google をセットアップするという項目を参考にGoogleアカウントで認証ができる状態にします。
Google 開発者コンソールに移動し、任意のGoogleアカウントでログインし、新しいプロジェクトを作成します。
「Social」 を検索し、「Google+ API」 を有効にします。
iOS 向け OAuth 2.0 Client ID を作成します。
[Credentials] > [Add Credentials] で、サービスアカウントを作成します。コンソールで、新しいパブリック/プライベートキーが作成されたことが警告されます。
Client ID の作成まで終了したら次はAmazon Cognitoにidentity poolの作成しましょう
identity poolの作成
AWSのコンソールにログインし、「Cognito」を選択します。
「Manage Federated Identities」をクリックします。
「Identity pool name」 に任意のアプリ名を入力します。
「Authentication providers」で「Google+」を選択し、Google 開発者コンソールから入手した「Client ID」を入力します。
「create pool」クリック時にAuthとUnAuthのIAMロールの作成も行なってくれます。
用意しているものがなければここで作成し、「Allow」をクリックしましょう。
S3 Bucketへの権限作成
IAM設定
AWS Management ConsoleからIAMを選択し、「Roles」をクリックします。 ここでCognitoの「create pool」時に作成されたAuthのRoleを選択します。
ポリシーにS3のPutの権限を付与しておきましょう。
こうすることで、Cognitoの認証がとったユーザーのみ、S3のPut権限を与えることができます。
iOSの実装
Googleサインインの設定
Google Sign-In for iOSに従って設定を行います。
GoogleServices-Info.plist
を作成するところから始めましょう。
任意のAppNameとiOSのBundle IDを入力し、「Choose and configure service」を選択します。
「ENABLE GOOGLE SIGN-IN」でサインインを有効にします。
GoogleServices-Info.plist
をダウンロードします。
サインイン処理の作成
サインイン画面の作成に従って作業を行います。
サインインを行うために CocoaPods
で Google/SignIn
SDKを組み込みます。
pod 'Google/SignIn'
GoogleServices-Info.plist
をXcodeに組み込みます。
Xcodeの 「Target」 > 「Info」 > 「URL Types」 に GoogleServices-Info.plist
の REVERSED_CLIENT_ID
の値を入力します。
AppDelegateの protocol
に GIDSignInDelegate
を追加します。
import GoogleSignIn @UIApplicationMain class AppDelegate: UIResponder, UIApplicationDelegate, GIDSignInDelegate {
didFinishLaunchingWithOptions
にsign-inの初期化処理を記載します。
YOUR_CLIENT_ID
には発行されたClient IDを入力します。
func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool { // sign-inの初期化処理 GIDSignIn.sharedInstance().clientID = "YOUR_CLIENT_ID" GIDSignIn.sharedInstance().delegate = self return true }
URLスキーマでアプリに遷移した際の処理をopenURLで行うようにします。
public func application(_ app: UIApplication, open url: URL, options: [UIApplicationOpenURLOptionsKey : Any] = [:]) -> Bool { return GIDSignIn.sharedInstance().handle(url as URL!, sourceApplication: options[UIApplicationOpenURLOptionsKey.sourceApplication] as? String, annotation: options[UIApplicationOpenURLOptionsKey.annotation]) }
AppDelegateに signIn
を実装します。
func sign(_ signIn: GIDSignIn!, didSignInFor user: GIDGoogleUser!, withError error: Error!) { if (error == nil) { // Perform any operations on signed in user here. let userId = user.userID // For client-side use only! let idToken = user.authentication.idToken // Safe to send to the server let fullName = user.profile.name let givenName = user.profile.givenName let familyName = user.profile.familyName let email = user.profile.email // ... } else { print("\(error.localizedDescription)") } }
サインイン画面の作成
ログイン画面に相当するUIViewControllerに GIDSignInUIDelegate
を適用します。
import GoogleSignIn class LoginViewController: UIViewController, GIDSignInUIDelegate { ... }
viewDidLoad
で初期化処理とサインインボタンの配置を行います。
GIDSignInButton
でボタンを配置するとテーマがGoogleのログインボタンになります。
override func viewDidLoad() { super.viewDidLoad() GIDSignIn.sharedInstance().uiDelegate = self ... // Signin Buttonの配置 let signInButton = GIDSignInButton() signInButton.center = view.center view.addSubview(_signInButton) }
GIDSignInUIDelegate
のメソッドを実装します。
// Stop the UIActivityIndicatorView animation that was started when the user // pressed the Sign In button func signInWillDispatch(signIn: GIDSignIn!, error: NSError!) { // stop indicator } // Present a view that prompts the user to sign in with Google func signIn(signIn: GIDSignIn!, presentViewController viewController: UIViewController!) { // signin self.present(viewController, animated: true, completion: nil) } // Dismiss the "Sign in with Google" view func signIn(signIn: GIDSignIn!, dismissViewController viewController: UIViewController!) { self.dismiss(animated: true, completion: nil) }
ここまでの作業で、ログイン画面を作ることができました。
Googleログイン後の認証情報の登録
「identity poolの作成」の項目で作成したidentity poolから「Identity pool ID」を確認します。
iOSで必要なAWS SDKを取得します。
pod 'AWSS3' pod 'AWSCognito' pod 'AWSCognitoIdentityProvider'
AWSIdentityProviderManagerのサブクラスを作成し、 logins
処理を仲介させるようにします。
// AWSIdentityProviderManagerのサブクラス class GoogleProvider: NSObject, AWSIdentityProviderManager { var tokens : [NSString : NSString]? init(tokens: [NSString : NSString]) { self.tokens = tokens } public func logins() -> AWSTask<NSDictionary> { let token: NSDictionary = NSDictionary(dictionary: tokens!) return AWSTask(result: token) } }
先ほどログイン画面を作る際に編集した AppDelegate
に認証成功後の処理を追加します。
YOUR_IDENTITY_POOL_ID
には先ほど確認した「Identity pool ID」を入力します。
import AWSCognito import AWSCognitoIdentityProvider import AWSCore import GoogleSignIn class AppDelegate: UIResponder, UIApplicationDelegate, GIDSignInDelegate { func sign(_ signIn: GIDSignIn!, didSignInFor user: GIDGoogleUser!, withError error: Error!) { if (error == nil) { // authentication token取得 let idToken = user.authentication.idToken // Initialize the Amazon Cognito credentials provider let provider = GoogleProvider(tokens: [AWSIdentityProviderGoogle as NSString: idToken as! NSString]) let credentialsProvider = AWSCognitoCredentialsProvider(regionType: .APNortheast1, identityPoolId:"ap-northeast-1:32e6c955-1859-4222-aa09-2f15a7ddbedc", identityProviderManager: provider) let configuration = AWSServiceConfiguration(region: .APNortheast1, credentialsProvider: credentialsProvider) AWSServiceManager.default().defaultServiceConfiguration = configuration } else { print("\(error.localizedDescription)") } } ...
AWSCognitoCredentialsProvider
の getIdentityId
をコールします。
credentialsProvider.getIdentityId().continueOnSuccessWith(block: { [weak self] task -> Any? in if credentialsProvider.identityId != nil { // login! } return nil })
AWS Management ConsoleからAWS CognitoのIdentity browserを確認すると、認証済みの端末が登録されていることが確認できます。
写真の送信
カメラの画像取得処理については割愛します。
画像を保存したURLから AWSS3 を使い、指定したS3のBucketへPutする処理を作成します。
func upload(imageURL: URL, createAt: Date) -> AWSTask<AnyObject> { let request = AWSS3TransferManagerUploadRequest() request?.bucket = "report-camera" request?.key = "images/original/\(createAt.timeIntervalSince1970)-\(imageURL.lastPathComponent)" request?.body = imageURL return AWSS3TransferManager.default().upload(request!) }
これをカメラの画像取得時に呼べば、S3に写真がPutされるようになります。
let data = UIImagePNGRepresentation(image) let paths = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask) let documentsDirectory = paths[0] let filename = documentsDirectory.appendingPathComponent("copy.png") try? data?.write(to: filename) upload(imageURL: filename, createAt: Date()).continueWith(block: { task -> Any? in print(task) })
まとめ
「Firebase SDK」を使った方がいいなど、色々ツッコミどころはあると思いますが、 Amazon CognitoはTwitter、facebookなどの認証にも対応しているため、S3へのPut以外にも応用はききます。
面倒な認証の実装を代理してもらうことで、アプリの実装により集中できるのではないでしょうか。